﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Diagnostics;

namespace Ooaavee.Events
{
    /// <summary>
    /// By using EventWatcher class you can listen to an any event sent by an any object.
    /// </summary>
    [DebuggerDisplay("{GetDebuggerDisplayMessage()}")]
    public class EventWatcher : IDisposable
    {
        #region Fields

        private readonly object _target;
        private readonly Dictionary<string, EventInfo> _events = new Dictionary<string, EventInfo>();
        private readonly EventHandlerFactory _factory = new EventHandlerFactory("EventWatcher");
        private readonly List<IEvent> _received = new List<IEvent>();

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the EventWatcher class and starts listening all public events sent 
        /// by the target object.
        /// </summary>
        /// <param name="target">the target object</param>
        public EventWatcher(object target)
        {
            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            _target = target;

            EventInfo[] evts = target.GetType().GetEvents();
            foreach (EventInfo evt in evts)
            {
                Subscribe(evt);
            }
        }

        /// <summary>
        /// Initializes a new instance of the EventWatcher class and starts listening one specified public 
        /// event sent by the target object.
        /// </summary>
        /// <param name="target">the target object</param>
        /// <param name="eventName">the name of an event</param>
        public EventWatcher(object target, string eventName)
        {
            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            if (eventName == null)
            {
                throw new ArgumentNullException("eventName");
            }

            _target = target;

            EventInfo evt = target.GetType().GetEvent(eventName);
            if (evt == null)
            {
                throw new ArgumentException("Event not found.");
            }
            Subscribe(evt);
        }

        /// <summary>
        /// Initializes a new instance of the EventWatcher class and starts listening one specified event 
        /// sent by the target object.
        /// </summary>
        /// <param name="target">the target object</param>
        /// <param name="eventName">the name of an event</param>
        /// <param name="bindingAttr">a bitmask comprised of one or more BindingFlags that specify how the search is conducted</param>
        public EventWatcher(object target, string eventName, BindingFlags bindingAttr)
        {
            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            if (eventName == null)
            {
                throw new ArgumentNullException("eventName");
            }

            _target = target;

            EventInfo evt = target.GetType().GetEvent(eventName, bindingAttr);
            if (evt == null)
            {
                throw new ArgumentException("Event not found.");
            }
            Subscribe(evt);
        }

        /// <summary>
        /// Initializes a new instance of the EventWatcher class and starts listening events sent by the target object.
        /// </summary>
        /// <param name="target">the target object</param>
        /// <param name="bindingAttr">a bitmask comprised of one or more BindingFlags that specify how the search is conducted</param>
        public EventWatcher(object target, BindingFlags bindingAttr)
        {
            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            _target = target;

            EventInfo[] evts = target.GetType().GetEvents(bindingAttr);
            foreach (EventInfo evt in evts)
            {
                Subscribe(evt);
            }
        }

        #endregion

        /// <summary>
        /// Gets a value indicating whether the EventWatcher has been disposed of.
        /// </summary>
        public virtual bool IsDisposed { get; protected set; }

        /// <summary>
        /// Gets a value indicating whether the EventWatcher has been suspended.
        /// </summary>
        public virtual bool IsSuspended { get; protected set; }

        /// <summary>
        /// Occurs when the target object raises an event.
        /// </summary>
        public event EventHandler<EventWatcherEventArgs> EventOccurred;

        /// <summary>
        /// Gets the results of this EventWatcher.
        /// </summary>
        /// <returns>THe results of this EventWatcher.</returns>
        public virtual EventWatcherResults GetResults()
        {
            if (IsDisposed)
            {
                throw new ObjectDisposedException("this");
            }

            return new EventWatcherResults { ReceivedEvents = _received.AsReadOnly() };
        }

        /// <summary>
        /// Temporarily suspends the event listening for the target object.
        /// </summary>
        public virtual void Suspend()
        {
            if (IsDisposed)
            {
                throw new ObjectDisposedException("this");
            }

            if (IsSuspended)
            {
                throw new InvalidOperationException("The EventWatcher is already suspended.");
            }

            IsSuspended = true;
        }

        /// <summary>
        /// Resumes the event listening.
        /// </summary>
        public virtual void Resume()
        {
            if (IsDisposed)
            {
                throw new ObjectDisposedException("this");
            }

            if (IsSuspended == false)
            {
                throw new InvalidOperationException("The EventWatcher is not suspended.");
            }

            IsSuspended = false;
        }

        /// <summary>
        /// Dispose.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Dispose.
        /// </summary>
        /// <param name="disposing">Disposing.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing && IsDisposed == false)
            {
                try
                {
                    if (_events.Count > 0)
                    {
                        EventInfo[] arr = new EventInfo[_events.Count];
                        _events.Values.CopyTo(arr, 0);
                        foreach (EventInfo evt in arr)
                        {
                            Unsubscribe(evt);
                        }
                    }
                }
                finally
                {
                    IsDisposed = true;
                }
            }
        }

        /// <summary>
        /// Raises the <see cref="EventOccurred"/> event.
        /// </summary>
        /// <param name="e">An <see cref="EventWatcherEventArgs"/> that contains the event data.</param>
        protected virtual void OnEventOccurred(EventWatcherEventArgs e)
        {
            _received.Add(e);

            if (EventOccurred != null)
            {
                EventOccurred(this, e);
            }
        }

        /// <summary>
        /// Starts listening specified event.
        /// </summary>
        /// <param name="evt">an event</param>
        private void Subscribe(EventInfo evt)
        {
            // Create the event handler
            object handler = _factory.GetEventHandler(evt);

            // Create a delegate, which points to the custom event handler
            Delegate custom = Delegate.CreateDelegate(evt.EventHandlerType, handler, "CustomEventHandler");

            // Link event handler to event
            evt.AddEventHandler(_target, custom);

            // Map our own event handler to the common event
            EventInfo commonEvt = handler.GetType().GetEvent("CommonEvent");
            Delegate common = Delegate.CreateDelegate(commonEvt.EventHandlerType, this, "MyEventCallback");
            commonEvt.AddEventHandler(handler, common);

            _events.Add(evt.Name, evt);
        }

        /// <summary>
        /// Stops listening specified event.
        /// </summary>
        /// <param name="evt">an event</param>
        private void Unsubscribe(EventInfo evt)
        {
            object handler = _factory.GetEventHandler(evt);
            EventInfo commonEvt = handler.GetType().GetEvent("CommonEvent");
            Delegate common = Delegate.CreateDelegate(commonEvt.EventHandlerType, this, "MyEventCallback");
            commonEvt.RemoveEventHandler(handler, common);

            _events.Remove(evt.Name);
        }

        /// <summary>
        /// Receives all events, see Subscribe and Unsubscribe methods.
        /// </summary>
        /// <param name="eventType">Event type</param>
        /// <param name="args">Event args</param>
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Receives all events, see Subscribe and Unsubscribe methods.")]
        private void MyEventCallback(Type eventType, object[] args)
        {
            if (IsSuspended || IsDisposed)
            {
                return;
            }

            EventInfo evt;
            if (_events.TryGetValue(eventType.Name, out evt) == false)
            {
                return;
            }

            EventWatcherEventArgs e = new EventWatcherEventArgs(evt, args);

            OnEventOccurred(e);

            if (e.Unsubscribe)
            {
                Unsubscribe(evt);
            }
        }

        #region Debugger

        /// <summary>
        /// Gets the results of this EventWatcher for the use of DebuggerDisplayAttribute.
        /// </summary>
        /// <returns>Results.</returns>
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "See DebuggerDisplayAttribute marked on this class.")]
        private string GetDebuggerDisplayMessage()
        {
            string s = string.Format("{0} event(s) received.", GetResults().EventCount);
            return s;
        }

        #endregion
    }
}